假如有閱讀前 27 天文章的讀者,可能會發現快取 ( Caching ) 這個詞時常出現在文章中,像是 Day 03 提到 ISR 會快取渲染結果;Day11 提到 Next 會快取 Server Components 渲染結果以及 Next 擴充了 fetch API,可以快取 data fetching 的 request 和 response。
為了能提升網頁效能,以及減少重複事項造成的資源浪費,Next 內建了許多快取機制。
我原本以為快取主要是「優化效能」,與功能開發的關係不大。直到有天看到一部很有趣的影片:I Hate Next.js 13 Caching Even More Now,才發現原來 Next 錯綜複雜的快取機制,可能會造成功能開發上一些令人一頭霧水的問題。
舉個例子:
假如我寫了一支 API,會隨機 respond 一個 1~1000 的用戶代碼:
/* app/api/hello/route.ts */
import { NextResponse } from 'next/server';
export function GET() {
const randomNumber = Math.floor(Math.random() * 1000);
const message = { message: `Hello user: ${randomNumber}` };
return NextResponse.json(message);
}
接著,我讓每個頁面,包含 Header 和 Footer 都 fetch 這支 API,並將 response 渲染在畫面上:
/* app/welcome/utils.ts */
export const getMessage = async () => {
const response = await fetch('http://localhost:3000/api/hello');
const json = await response.json();
return await json.message;
}
/* app/welcome/page.tsx */
import Footer from './components/Footer';
import Header from './components/Header';
import { getMessage } from '@/utils';
// /dashboard 和 /shop 也渲染 getMessage() return 的值
export default async function Page() {
const message = await getMessage();
return (
<>
{/* Header 和 Footer 也渲染相同資料 */}
<Header />
{message}
<Footer />
<>
);
}
照理來說,每支 API response 的數字會不同,所以每個頁面,包含 Header 和 Footer 的數字都要不同,但實際操作網頁卻發現:
怎麼去到哪都是同一個數字 262?!
本系列的倒數第三篇文章,我們就來一探究竟 Next.js 中既貼心,又常害開發者不知所措的快取機制吧!
我們可以大致依據快取拜訪的時機,和影響範圍分為四種機制。Scope 有小到大依序為 Data Cache、Request Memoization、Full Route Cache、Router Cache。
運作機制簡單來說,當 request 進來後,Next 會逐層查看快取有沒有可用資料,沒有就往下層找,假如都沒有快取才向 data source 拿資料,拿到後再逐層建立快取,可參考下圖:
( 圖片來源:https://nextjs.org/docs/app/building-your-application/caching )
開始之前一個小補充,以下內容提到的頁面轉換都是透過
<a>
而不是<Link>
,原因明天會分享
Next 預設會快取 server data fetching,從 data source 取得的結果,當下次有相同 request 進來時,就不用再到 data source 拿資料,可以直接從快取拿。
所以合理懷疑,前言的範例中,每頁數字都一樣是 Data Cache 搞的鬼。那我們接著來禁用 Data Cache。
禁用 Data Cache
假設想禁用 Data Cache,可以讓 cache
option 為 no-store
:
/* app/utils.tsx */
export const getMessage = async () => {
const response = await fetch('http://localhost:3000/api/hello', {
cache: 'no-store', // 禁用快取
});
return await response.json();
};
禁用 Data Cache 後,相同實驗再做一次:
的確每次進到頁面的用戶代碼會不同了。所以當禁用 Data Cache 後,每次換頁呼叫 getMessage()
,快取沒有資料,就會重新進行 data fetching。
重置 Data Cache
假如想要重製 Data Cache,有兩種方式:
固定時間重置
假如資料相對固定,不須頻繁重置 Data Cache,可以使用 next.revalidate
option 在固定時間重置 Data Cache,比方說設定兩秒後重置:
/* app/utils.tsx */
export const getMessage = async () => {
const response = await fetch('http://localhost:3000/api/bbbb', {
next: { revalidate: 2 },
});
return await response.json();
};
完成後,我們持續重新整理頁面,觀察頁面變化:
會發現每兩秒多一點,數字會更新一次:
所以在 revalidation 期間收到 request,Next 都會 return 快取版本;假如過了 revalidation 時間,Next 依舊會先 return 快取版本,接著再重新 fetch 資料並將結果存快取,下次 request 就會拿到新版本。
( 圖片來源:https://nextjs.org/docs/app/building-your-application/caching )
主動觸發重置
假如想主動觸發 revalidation,可以使用 Day 23 介紹 Server Actions 時提到的 revalidatePath
或 revalidateTag
。
兩個 function 可以用在 Route Hanlder 或 Server Actions 中。我們來嘗試在 /welcome 加一個按鈕,點擊後會觸發 revalidatePath
:
/* app/welcome/app.tsx */
import { revalidatePath } from 'next/cache';
const revalidate = async () => {
'use server';
revalidatePath('/welcome');
};
export default async function Page() {
...
return (
<>
...
<form action={revalidate}>
<button type='submit'>Revalidate</button>
</form>
</>
);
}
當點擊 revalidate 時,數字就會更新:
篇幅關係就不介紹
revalidatePath
,有興趣的讀者可參考官方文件
所以觸發 revalidatePath
後,會先清除 Data Cache,接著收到要 re-render route segment 的 request 後,重新 fetch data,並存一份新的快取:
( 圖片來源:https://nextjs.org/docs/app/building-your-application/caching )
認識完 Data Cache 後,眼尖的讀者可能會發現,照理來說,頁面中的 Header、Footer、和 Page Component 會發三次 request,假如沒有 Data Cache,應該要拿到三個不同的數字,但為什麼禁用 Data Cache 後,三者的數字還是相同?
這就要講到 Data Cache 往上一層的快取機制 - Request Memoization。
React 擴充了 fetch API,假如在 Server Components 中發送 fetch request,React 會快取 request 和 response。當渲染同個路由中的其他 components 時,假設有收到相同 URL 和 options 的 request,就可以直接從快取得到 response,不用再執行一次 data fetching。
回頭來看上一段最後的問題,雖然我們在 /welcome 的 Header、Footer、Page Component 都發送 fetch request,但因為 request 相同,data fetching 只會執行一次。 當 React 處理完第一個請求後,會快取結果,接下來兩個請求就會直接從快取拿 response。所以三者才會拿到一樣的用戶代碼。
( 圖片來源:https://nextjs.org/docs/app/building-your-application/caching#request-memoization )
透過 request memoization,假如在不同 components 要用到同一支 API 的 response,就不用為了減少 server 負擔而將 response 透過 props 傳到其他 components,可以放心重複呼叫 fetch request。
禁用 Requeest Memoization
要禁用 request memoization,可以將 AbortController
物件中的 signal
傳進 fetch request 中:
const { signal } = new AbortController;
fetch(url, { signal });
我們來同時禁用 Data Cache 和 Request Memoization,看看會發生什麼改變:
/* app/utils.ts */
const { signal } = new AbortController();
export const getMessage = async () => {
const response = await fetch('http://localhost:3000/api/llll', {
signal,
cache: 'no-store',
});
const json = await response.json();
return await json.message;
};
每次重新整理,Header、Footer 和 Page Component 的用戶代碼就會不相同:
注意事項:
- Request memoization 只適用 GET request,其他類型不適用
- Request memoization 只適用 React Component Tree,不適用 Route Handler
- 假如無法使用 fetch API (ex: GraphQL clients),可以使用 React 的 cache function 去快取 function return 的值
來整合與比較一下 Data Cache 和 Request Memoization:
當 server 渲染一個 route segment,收到 fetch request 時,會先檢查有沒有 Request Memoization。假如有,就不會執行 data fetching,直接 return 快取的 fetch result。
假如沒有 request memoization,則 data fetching 時,會先檢查有沒有 Data Cache。假如有,就直接 response 快取內容;假如沒有,才到 data source 重新撈取資料。
( 圖片來源:https://nextjs.org/docs/app/building-your-application/caching#request-memoization )
除了拜訪的時機不同,Data Cache 和 Request Memoization 的作用域和目的也不同。
Data Cache 主要目的為避免重複像 data source 索取相同資料,可以跨路由取用。所以開頭的範例,不管在哪個頁面,呼叫 getMessage()
得到的結果都是相同的數字 262。
Request Memoization 主要目的是避免渲染時重複呼叫相同 request,只適用於單個 route segment 渲染過程。當 route segment 渲染完成後,便會清除快取,因此不需要 revalidate。
所以就算禁用 Data Cache,依然會執行 Request Memoization。這也是為什麼,當我們禁用 Data Cache 時,雖然拜訪其他頁面用戶代碼會變,但每頁的 Header、Footer 和 Page Component 的用戶代碼仍然相同。
最後來做個補充:
還記得 Day 11 有提到,可以使用 router.refresh()
,讓 Server Components 重新進行 data fetching 和渲染嗎?
那 router.refresh
和今天提到的 revalidatePath
有什麼不同呢?主要差別在,兩者會影響到的快取層不同。
使用 router.refresh
,只會重置 client 端的快取 - Router Cache ( 明天會介紹 ),不會重置 server 端的 Full Route Cache 和 Data Cache。
一樣做個小實驗,我們讓 /welcome 的 Refresh 按鈕,點擊後會觸發 router.refresh()
,觀察會發生什麼事:
因為 server 端的快取都沒有重置,所以 Server Components 重新 fetch data 和渲染依然會得到相同結果。
但假如使用 revalidatePath
,Data Cache 和 Full Route Cache 會重置,所以畫面的數字會改變:
今天就先到這邊,希望以上內容能讓讀者碰到 data fetching 結果不如預期時,有多一些線索。
明天會再介紹另外兩個 Caching 機制 - Full Route Cache 和 Router Cache,謝謝大家耐心的閱讀,我們明天見!